Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Enhanced ECDSA signature support for ES256, ES384, and ES512 algorithms with improved encoding and decoding capabilities.
    • Implemented cryptographic provider ordering mechanism to prioritize providers by priority level.
  • Tests

    • Added test coverage for ECDSA algorithm signature conversions.
  • Chores

    • Added test dependencies.

…tory

closes #33831

Signed-off-by: mposolda <mposolda@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

The changes introduce ECDSA signature encoding/decoding capabilities to the authorization client module via new ASN.1 utilities and a CryptoProvider implementation. A provider ordering mechanism is added to the CryptoProvider interface and implemented across existing crypto providers. Test dependencies and initialization hooks are added to support the new functionality.

Changes

Cohort / File(s) Summary
Authorization Client Crypto Infrastructure
authz/client/pom.xml, authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
Added test-scoped dependencies (junit, hamcrest) and integrated CryptoIntegration initialization in AuthzClient.create().
ASN.1 Encoding/Decoding Utilities
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Encoder.java, authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java
New package-private ASN.1 DER encoder and decoder classes for serializing/deserializing INTEGER and SEQUENCE structures with proper length encoding.
Authorization Client Crypto Provider
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java, authz/client/src/main/resources/META-INF/services/org.keycloak.common.crypto.CryptoProvider
New public CryptoProvider implementation with ECDSA support (concatenated RS to/from ASN.1 DER conversion) and KeyStore access; most operations marked as unsupported. Registered via service loader.
ECDSA Signature Test
authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java
New test class verifying round-trip ECDSA signature conversions between DER and concatenated RS formats for ES256, ES384, and ES512.
Crypto Provider Interface & Integration
common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java, common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java
Added order() method to CryptoProvider interface. Updated CryptoIntegration to sort providers by order in descending priority and select the highest-priority provider.
Crypto Provider Implementations
crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java, crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java, crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java
Added order() method returning 200 to each existing crypto provider implementation to establish provider precedence.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 ASN.1 hops through bytes with glee,
ECDSA signatures dance wild and free,
Providers now prioritize, ordered just right,
Crypto integration shines oh-so-bright!
Round-trip conversions, tested with care,
New cryptographic spring is in the air!

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (2 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: feature-authz-crypto-implementation' is vague and generic, using non-descriptive terms that don't convey meaningful information about the actual changeset. Replace with a descriptive title that captures the main changes, e.g., 'Add ECDSA crypto support and ASN.1 encoding/decoding utilities for authorization client'.
Description check ❓ Inconclusive The description only mentions automated creation and batch status without explaining the actual purpose, scope, or impact of the changes in the pull request. Provide context about what cryptographic utilities were added, why they are needed, and how they integrate with the authorization client and crypto framework.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-authz-crypto-implementation

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java`:
- Around line 127-167: The method ASN1Decoder.readLength() currently returns -1
to signal indefinite-length encoding (0x80), but callers like readSequence() and
readInteger() do not handle -1 and can misbehave; change readLength() to throw
an IOException when encountering 0x80 (indefinite-length) instead of returning
-1 so callers get a clear error path consistent with other validation checks;
update any error message to indicate "indefinite-length encoding (0x80) not
supported" and rely on existing exception handling in readSequence() and
readInteger() to avoid negative array sizes or silent empty reads.

In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java`:
- Around line 114-115: Remove the two dead ASN1Encoder.create().write(...) calls
that discard results; in AuthzClientCryptoProvider replace the redundant
ASN1Encoder.create().write(rBigInteger) and
ASN1Encoder.create().write(sBigInteger) invocations with only the actual
encoding path that uses the encoder instance later (the code that performs the
real encoding for rBigInteger and sBigInteger), ensuring you keep the working
encoder usage and remove the discarded temporary encoder calls.

In
`@authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java`:
- Line 54: Rename the misspelled variable asn1Des to asn1Der in
ECDSAAlgorithmTest.java where the result of
prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat,
algorithm.getSignatureLength()) is assigned; update all usages of asn1Des in the
test to use asn1Der so the variable name correctly reflects the ASN.1 DER format
returned by concatenatedRSToASN1DER.

In `@common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java`:
- Around line 64-70: The logging uses logger.debugf with an unsanitized format
string and the loop leaves a trailing comma; in CryptoIntegration when handling
foundProviders, replace logger.debugf(builder.toString()) with logger.debug(...)
and build the provider list without a trailing ", " (e.g., append separators
conditionally or use String.join over
foundProviders.stream().skip(1).map(p->p.getClass().getName()).collect(...)) so
the output is printed raw and has no trailing separator.

In `@common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java`:
- Around line 39-44: The new non-default method on the public interface
CryptoProvider (int order()) will break external implementations; change it to a
default method (default int order()) that returns a safe default (e.g., 0) so
existing third-party implementations remain compatible while internal classes
(AuthzClientCryptoProvider, DefaultCryptoProvider, FIPS1402Provider,
WildFlyElytronProvider) can keep their explicit overrides with 100/200.
🧹 Nitpick comments (4)
authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java (1)

49-63: Sequence element length tracking may miscount with long-form tags/lengths.

In readSequence(), the content length returned by readLength() represents the total bytes of the sequence's content (all child TLVs combined). The loop subtracts bytes.length (the full TLV of each child element returned by readNext()) which is correct.

However, readNext() re-reads the tag and length by resetting the stream. This is clever but tightly couples the correctness to count being accurate across all internal reads. Consider adding a brief comment explaining this invariant for maintainability.

authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java (1)

41-43: Single default-size EC key used for all three algorithm tests.

KeyPairGenerator.getInstance("EC").genKeyPair() uses the JDK's default curve (typically P-256). ES384 expects P-384 and ES512 expects P-521. The round-trip encoding test still passes because integerToBytes pads/trims, but R and S values will always be ≤32 bytes, so the larger-format paths are only exercised with zero-padded data rather than realistic large-coordinate values.

Consider generating an appropriately-sized key per algorithm (e.g., pass curve name to ECGenParameterSpec) to improve coverage.

authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java (2)

100-164: Consider caching the ECDSACryptoProvider instance.

getEcdsaCryptoProvider() allocates a new anonymous class on every call. Since it's stateless, returning a singleton field avoids needless allocations on hot paths.


60-67: getBouncyCastleProvider() returns the default KeyStore provider, not BouncyCastle.

This is understandable given the minimal-provider intent, but it may confuse callers that expect an actual BC Provider. A brief Javadoc note clarifying the deliberate non-BC behavior would help future readers.

Comment on lines +127 to +167
int readLength() throws IOException {
int length = read();
if (length < 0) {
throw new EOFException("EOF found when length expected");
}

if (length == 0x80) {
return -1; // indefinite-length encoding
}

if (length > 127) {
int size = length & 0x7f;

// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
if (size > 4) {
throw new IOException("DER length more than 4 bytes: " + size);
}

length = 0;
for (int i = 0; i < size; i++) {
int next = read();

if (next < 0) {
throw new EOFException("EOF found reading length");
}

length = (length << 8) + next;
}

if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}

if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}
}

return length;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Indefinite-length encoding (0x80) is recognized but not safely handled by callers.

readLength() returns -1 for indefinite-length encoding. In readSequence(), this silently returns an empty list (the while (length > 0) loop never executes). In readInteger(), it causes a NegativeArraySizeException from new byte[-1]. Since DER prohibits indefinite-length encoding, throwing an IOException here instead of returning -1 would be more defensive and consistent with the other error paths in this method.

Proposed fix
         if (length == 0x80) {
-            return -1;      // indefinite-length encoding
+            throw new IOException("Indefinite-length encoding is not supported in DER");
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int readLength() throws IOException {
int length = read();
if (length < 0) {
throw new EOFException("EOF found when length expected");
}
if (length == 0x80) {
return -1; // indefinite-length encoding
}
if (length > 127) {
int size = length & 0x7f;
// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
if (size > 4) {
throw new IOException("DER length more than 4 bytes: " + size);
}
length = 0;
for (int i = 0; i < size; i++) {
int next = read();
if (next < 0) {
throw new EOFException("EOF found reading length");
}
length = (length << 8) + next;
}
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}
}
return length;
}
int readLength() throws IOException {
int length = read();
if (length < 0) {
throw new EOFException("EOF found when length expected");
}
if (length == 0x80) {
throw new IOException("Indefinite-length encoding is not supported in DER");
}
if (length > 127) {
int size = length & 0x7f;
// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
if (size > 4) {
throw new IOException("DER length more than 4 bytes: " + size);
}
length = 0;
for (int i = 0; i < size; i++) {
int next = read();
if (next < 0) {
throw new EOFException("EOF found reading length");
}
length = (length << 8) + next;
}
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}
}
return length;
}
🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/ASN1Decoder.java`
around lines 127 - 167, The method ASN1Decoder.readLength() currently returns -1
to signal indefinite-length encoding (0x80), but callers like readSequence() and
readInteger() do not handle -1 and can misbehave; change readLength() to throw
an IOException when encountering 0x80 (indefinite-length) instead of returning
-1 so callers get a clear error path consistent with other validation checks;
update any error message to indicate "indefinite-length encoding (0x80) not
supported" and rely on existing exception handling in readSequence() and
readInteger() to avoid negative array sizes or silent empty reads.

Comment on lines +114 to +115
ASN1Encoder.create().write(rBigInteger);
ASN1Encoder.create().write(sBigInteger);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dead code: two ASN1Encoder instances created and immediately discarded.

Lines 114-115 create encoders, write R and S, and throw away the results. The actual encoding is performed on lines 117-121. These are likely leftover from development.

Proposed fix — remove dead lines
                 BigInteger rBigInteger = new BigInteger(r);
                 BigInteger sBigInteger = new BigInteger(s);

-                ASN1Encoder.create().write(rBigInteger);
-                ASN1Encoder.create().write(sBigInteger);
-
                 return ASN1Encoder.create()
                         .writeDerSeq(
                                 ASN1Encoder.create().write(rBigInteger),
🤖 Prompt for AI Agents
In
`@authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java`
around lines 114 - 115, Remove the two dead ASN1Encoder.create().write(...)
calls that discard results; in AuthzClientCryptoProvider replace the redundant
ASN1Encoder.create().write(rBigInteger) and
ASN1Encoder.create().write(sBigInteger) invocations with only the actual
encoding path that uses the encoder instance later (the code that performs the
real encoding for rBigInteger and sBigInteger), ensuring you keep the working
encoder usage and remove the discarded temporary encoder calls.

signature.update(data);
byte[] sign = signature.sign();
byte[] rsConcat = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(sign, algorithm.getSignatureLength());
byte[] asn1Des = prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat, algorithm.getSignatureLength());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: asn1Desasn1Der.

🤖 Prompt for AI Agents
In
`@authz/client/src/test/java/org/keycloak/authorization/client/test/ECDSAAlgorithmTest.java`
at line 54, Rename the misspelled variable asn1Des to asn1Der in
ECDSAAlgorithmTest.java where the result of
prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat,
algorithm.getSignatureLength()) is assigned; update all usages of asn1Des in the
test to use asn1Der so the variable name correctly reflects the ASN.1 DER format
returned by concatenatedRSToASN1DER.

Comment on lines +64 to +70
if (foundProviders.size() > 1) {
StringBuilder builder = new StringBuilder("Ignored crypto providers: ");
for (int i = 1 ; i < foundProviders.size() ; i++) {
builder.append(foundProviders.get(i).getClass().getName() + ", ");
}
logger.debugf(builder.toString());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

logger.debugf with unsanitized format string; also trailing comma in output.

Two minor issues:

  1. logger.debugf(builder.toString()) treats the string as a format pattern. If any provider class name ever contains %s or %d, this will break or produce garbled output. Use logger.debug(builder.toString()) instead.
  2. The loop always appends ", " after each provider name, leaving a trailing separator.
Proposed fix
             if (foundProviders.size() > 1) {
-                StringBuilder builder = new StringBuilder("Ignored crypto providers: ");
-                for (int i = 1 ; i < foundProviders.size() ; i++) {
-                    builder.append(foundProviders.get(i).getClass().getName() + ", ");
-                }
-                logger.debugf(builder.toString());
+                List<String> ignored = foundProviders.subList(1, foundProviders.size()).stream()
+                        .map(p -> p.getClass().getName())
+                        .collect(Collectors.toList());
+                logger.debugf("Ignored crypto providers: %s", ignored);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (foundProviders.size() > 1) {
StringBuilder builder = new StringBuilder("Ignored crypto providers: ");
for (int i = 1 ; i < foundProviders.size() ; i++) {
builder.append(foundProviders.get(i).getClass().getName() + ", ");
}
logger.debugf(builder.toString());
}
if (foundProviders.size() > 1) {
List<String> ignored = foundProviders.subList(1, foundProviders.size()).stream()
.map(p -> p.getClass().getName())
.collect(Collectors.toList());
logger.debugf("Ignored crypto providers: %s", ignored);
}
🤖 Prompt for AI Agents
In `@common/src/main/java/org/keycloak/common/crypto/CryptoIntegration.java`
around lines 64 - 70, The logging uses logger.debugf with an unsanitized format
string and the loop leaves a trailing comma; in CryptoIntegration when handling
foundProviders, replace logger.debugf(builder.toString()) with logger.debug(...)
and build the provider list without a trailing ", " (e.g., append separators
conditionally or use String.join over
foundProviders.stream().skip(1).map(p->p.getClass().getName()).collect(...)) so
the output is printed raw and has no trailing separator.

Comment on lines +39 to +44
/**
* Order of this provider. This allows to specify which CryptoProvider will have preference in case that more of them are on the classpath.
*
* The higher number has preference over the lower number
*/
int order();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for CryptoProvider implementations
rg -nP --type=java 'implements\b.*\bCryptoProvider\b' -C2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2598


🏁 Script executed:

#!/bin/bash
# Check if the CryptoProvider implementations have order() method
rg -nP --type=java 'order\(\)' crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java authz/client/src/main/java/org/keycloak/authorization/client/util/crypto/AuthzClientCryptoProvider.java -C3

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2900


🏁 Script executed:

#!/bin/bash
# Check the CryptoProvider interface definition and its module visibility
cat common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java | head -50

# Also check if there are any module-info files or access modifiers
rg -nP --type=java 'public.*interface\s+CryptoProvider' common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java -B5 -A10

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2435


Adding a non-default method to a public interface breaks external implementations.

While this is a breaking change for third-party implementations of CryptoProvider, all internal implementations in this codebase (AuthzClientCryptoProvider, DefaultCryptoProvider, FIPS1402Provider, and WildFlyElytronProvider) have already been properly updated with implementations returning their respective order values (100 or 200).

If backward compatibility with external implementations is required, provide a default implementation:

Proposed fix
-    int order();
+    default int order() {
+        return 0;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Order of this provider. This allows to specify which CryptoProvider will have preference in case that more of them are on the classpath.
*
* The higher number has preference over the lower number
*/
int order();
/**
* Order of this provider. This allows to specify which CryptoProvider will have preference in case that more of them are on the classpath.
*
* The higher number has preference over the lower number
*/
default int order() {
return 0;
}
🤖 Prompt for AI Agents
In `@common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java` around
lines 39 - 44, The new non-default method on the public interface CryptoProvider
(int order()) will break external implementations; change it to a default method
(default int order()) that returns a safe default (e.g., 0) so existing
third-party implementations remain compatible while internal classes
(AuthzClientCryptoProvider, DefaultCryptoProvider, FIPS1402Provider,
WildFlyElytronProvider) can keep their explicit overrides with 100/200.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant